using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Manoso.Data.FluentMap
{
    internal interface IMapSql
    {
        IRelation Relation { get; set; }
        Type ValueType { get; set; }
        string Alias { get; set; }
        string TableAlias { get; }
        IList<IMapProperty> Columns { get; }
        IList<IMapProperty> Keys { get; }
    }

    public interface IMapSql<out T>
    {
        /// <summary>
        /// Generate command for T object if predicate returns true.
        /// </summary>
        /// <param name="predicate">Predicate for T.</param>
        void Match(Func<T, bool> predicate);

        /// <summary>
        /// Finalise the T object before generating command.  
        /// </summary>
        /// <param name="action">The finalise action.</param>
        void Fix(Action<T> action);
    }

    internal abstract class MapSql<T> : IMapSql<T>, IMapSql
    {
        public IRelation Relation { get; set; }
        public Type ValueType { get; set; }
        public string Alias { get; set; }
        public string Table { get; set; }
        public string Target { get; internal set; }
        public bool IsTable { get; internal set; }

        public bool IsExtension { get { return _tableAlias != null; } }

        protected string _tableAlias;
        public string TableAlias
        {
            get { return _tableAlias ?? Alias; }
        }

        private readonly IList<IMapProperty> _keys = new List<IMapProperty>();
        public virtual IList<IMapProperty> Keys
        {
            get { return _keys; }
        }

        private readonly IList<IMapProperty> _columns = new List<IMapProperty>();
        public virtual IList<IMapProperty> Columns
        {
            get { return _columns; }
        }

        public IMapProperty MapTstamp { get; protected set; }

        protected static readonly Func<T, bool> NOT_MATCH = x => false;

        protected Func<T, bool> IsMatch { get; set; }

        protected Action<T> ActionFix { get; set; }

        public void Match(Func<T, bool> predicate)
        {
            IsMatch = predicate ?? NOT_MATCH;
        }

        public void Fix(Action<T> action)
        {
            ActionFix = action;
        }

        public void AddMap(IMapProperty map, bool isId = false)
        {
            if (map == null) return;

            if (isId)
            {
                _keys.Add(map);
            }
            else
            {
                _columns.Add(map);
            }
        }

        public void AddMaps(IEnumerable<IMapProperty> maps, bool isId = false)
        {
            if (maps == null) return;

            if (isId)
            {
                ((List<IMapProperty>)_keys).AddRange(maps);
            }
            else
            {
                ((List<IMapProperty>)_columns).AddRange(maps);
            }
        }


        internal IEnumerable<IMapProperty> CreateProperties(IEnumerable<MemberInfo> infos = null)
        {
            if (infos == null)
                infos = ValueType.GetDataMembers(p =>
                {
                    var dataType = p.DataType();
                    return dataType != null
                           &&
                           (dataType.IsValueType || dataType == typeof(string) || dataType == typeof(Guid) ||
                            dataType == typeof(byte[]));
                });
            return infos
                .Select(CreateProperty)
                .Where(p => p != null && !string.IsNullOrEmpty(p.Column.Name));
        }

        internal virtual IMapProperty CreateProperty(MemberInfo info)
        {
            var memberType = info.GetMemberType();
            if (memberType == null) return null;

            var mp = new MapProperty
            {
                HostAlias = TableAlias,
                PropertyType = memberType,
                PropertyName = info.Name,
                Column = new MapColumn(info.Name, Alias),
                Set = info.SetValue,
            };
            mp.Load = x => mp.Value = GetPropertyValue(info, x);
            return mp;
        }

        internal IEnumerable<IMapProperty> CreateIds()
        {
            return CreateProperties(Relation.Ids);
        }

        protected virtual void LoadIds(IMapTask task)
        {
            if (Keys.Count == 0 || Keys.Count == 1 && Keys.First() == MapTstamp)
            {
                var ids = CreateIds();
                AddMaps(ids, true);
            }

            foreach (var mp in Keys)
            {
                if (mp.Load != null)
                    mp.Load(task.Values[mp.HostAlias]);
            }
        }

        protected IMapProperty GetDeclare(IMapTask task, IList<string> stack, bool isInsert = false, bool stackOnly = false)
        {
            var declares = task.Declares;

            LoadIds(task);

            var ids = Keys;
            if (ids.Count() != 1) return null;

            var id = ids.First();
            var idParam = id.Column.Alias;

            var declare = task.Adaptor.GetDeclare(id.PropertyType, idParam);
            if ((isInsert && stackOnly || !isInsert) && stack.All(x => !string.Equals(x, declare)))
                stack.Add(declare);

            if (declares != null && declares.Contains(idParam)) return id;
            if (declares != null && !stackOnly)
                declares.Add(idParam);

            if (!stackOnly)
                task.AddCommand(declare, MapCommandType.Declare);

            return id;
        }

        protected void SetAssignment(IMapTask task, IList<string> stack, IMapProperty id, string assignment)
        {
            var lookup = task.Adaptor.GetDeclare(id.PropertyType, id.Column.Alias);

            var dump = false;
            var copy = new List<string>(stack);

            foreach (var item in copy)
            {
                if (dump)
                {
                    stack.Remove(item);
                    continue;
                }
                dump = item == lookup;
            }

            stack.Add(assignment);
        }

        protected virtual object GetPropertyValue(MemberInfo info, object value)
        {
            return info.GetValue(value);
        }

    }
}